1

原文地址

周五组内同学讨论搞一些好玩的东西,有人提到了类似『5分钟实现koa』,『100行实现react』的创意,仔细想了以后,5分钟实现koa并非不能实现,遂有了这篇博客。

准备

先打开koa官网,随意找出了一个代表koa核心功能的的demo就可以,如下

const Koa = require('koa');
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

最终要实现的效果是实现的一个5min-koa模块,直接将代码中第一行替换为const Koa = require('./5min-koa');,程序可以正常执行就可以了。

Koa的核心

通过koa官网得知,app.listen方法实际上是如下代码的简写

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);

所以我们可以先把app.listen实现出来

class Koa {
  constructor() {}
  callback() {
    return (req, res) => {
      // TODO
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

koa的核心分为四部分,分别是

  • context 上下文
  • middleware 中间件
  • request 请求
  • responce 响应

Context

我们先来实现一个最简化版的context,如下

class Context {
  constructor(app, req, res) {
    this.app = app
    this.req = req
    this.res = res
    // 为了尽可能缩短实现时间,我们直接使用原生的res和req,没有实现ctx上的ctx.request ctx.response
    // ctx.request ctx.response只是在原生res和req上包装处理了一层
  }
  // 实现一些demo中使用到的ctx上代理的方法
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

这样就完成了一个最基本的Context,别看小,已经够用了。
每一次有新的请求,都会创建一个新的ctx对象。

Middleware

koa的中间件是一个异步函数,接受两个参数,分别是ctx和next,其中ctx是当前的请求上下文,next是下一个中间件(也是异步函数),这样想来,我们需要一个维护中间件的数组,每次调用app.use就是往数组中push一个一步函数。所以use方法实现如下

use(middleware) {
  this.middlewares.push(middleware)
}

每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。单单灌进ctx还不够,还要使每个中间件都能通过next函数调用到下一个中间件。当我们调用next函数时,一般是不需要传参数的,而被调用的中间件中一定会接收到ctx和next两个参数。

调用方不需要传参,被调用方却能接到参数,这让我立刻想到bind方法,只要将每一个中间件所需要的ctx和next都提前绑定好,问题就解决了。下面的代码就是通过bind方法,将用户传入的middleware列表转换成next函数列表

let bindedMiddleware = []

for (let i = middlewares.length - 1; i >= 0; i--) {
  if (middlewares.length == i + 1) {
    // 最后一个中间件,next方法设置为Promise.resolve
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
  } else {
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
  }
}

最后我们就得到了一个next函数数组,也就是bindedMiddleware这个变量了。

Request

http.createServer中的回调函数,每次接收到请求的时候会被调用,所以我们在上面callback方法的TODO位置,编写处理请求的代码, 并将上面的middleware列表转next函数列表的代码放入其中。

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = []
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
      }
    }
    return bindedMiddleware[0]()
  } else {
    return Promise.resolve()
  }
}

Responce

我们简单出来下相应就好了,直接将ctx.body发送给客户端。

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
    ctx.res.end(ctx.body);
  }
}

完成Koa类的实现

koa的app实例上面带有on,emit等方法,这是node events模块实现好的东西。直接让Koa类继承自events模块就好了。
我们再将上面实现出来的handleRequest和handleResponse方法放入koa类的callback方法中,得到最终我们实现的Koa,一共58行代码,如下

const http = require('http');
const Emitter = require('events');

class Context {
  constructor(app, req, res) {
    this.app = app;
    this.req = req;
    this.res = res;
  }
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

class Koa extends Emitter{
  constructor(options) {
    super();
    this.options = options
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback() {
    return (req, res) => {
      let ctx = new Context(this, req, res);
      handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = [];
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
      }
    }
    return bindedMiddleware[0]();
  } else {
    return Promise.resolve();
  }
}

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
    ctx.res.end(ctx.body);
  }
}

module.exports = Koa;

试试跑一下篇首的Demo,没什么问题。

结语

简版实现,码糙理不糙,展示出了koa核心的东西,但少了错误处理,也完全没有考虑性能啥的,需要完善的地方还很多很多。

笔者在写了这个5分钟koa以后去看了koa源码,发现实现思路基本就是这样,相信经过我的这个5分钟koa的洗礼,你去看koa源码一样小菜一碟。

Done!


GeoffZhu
763 声望50 粉丝

front-end